// Copyright IBM Corp. and LoopBack contributors 2019,2020. All Rights Reserved.
// Node module: @loopback/cli
// This file is licensed under the MIT License.
// License text available at https://opensource.org/licenses/MIT

'use strict';

const fse = require('fs-extra');
const semver = require('semver');
const chalk = require('chalk');
const latestVersion = require('latest-version');

const cliPkg = require('../package.json');
const g = require('./globalize');
const templateDeps = cliPkg.config.templateDependencies;

/**
 * Print @loopback/* versions
 * @param log - A function to log information
 */
function printVersions(log = console.log) {
  const ver = cliPkg.version;
  log('@loopback/cli version: %s', ver);
  log('\n@loopback/* dependencies:');
  for (const d in templateDeps) {
    if (d.startsWith('@loopback/') && d !== '@loopback/cli') {
      log('  - %s: %s', d, templateDeps[d]);
    }
  }
}

/**
 * Check project dependencies against module versions from the cli template
 * @param generator - Yeoman generator instance
 */
async function checkDependencies(generator) {
  const pkg = generator.fs.readJSON(generator.destinationPath('package.json'));

  const isUpdate = generator.command === 'update';
  const pkgDeps = pkg
    ? {
        dependencies: {...pkg.dependencies},
        devDependencies: {...pkg.devDependencies},
        peerDependencies: {...pkg.peerDependencies},
      }
    : {};

  if (!pkg) {
    if (isUpdate) {
      printVersions(generator.log);
      await checkCliVersion(generator.log);
      return;
    }
    const err = new Error(
      'No package.json found in ' +
        generator.destinationRoot() +
        '. ' +
        'The command must be run in a LoopBack project.',
    );
    generator.exit(err);
    return;
  }

  const dependentPackage = '@loopback/core';

  const projectDepsNames = isUpdate
    ? Object.keys(
        // Check dependencies, devDependencies, and peerDependencies
        {
          ...pkgDeps.dependencies,
          ...pkgDeps.devDependencies,
          ...pkgDeps.peerDependencies,
        },
      )
    : Object.keys(pkgDeps.dependencies);

  const isLBProj = isUpdate
    ? projectDepsNames.some(n => n.startsWith('@loopback/'))
    : projectDepsNames.includes(dependentPackage);

  if (!isLBProj) {
    const err = new Error(
      'No `@loopback/core` package found in the "dependencies" section of ' +
        generator.destinationPath('package.json') +
        '. ' +
        'The command must be run in a LoopBack project.',
    );
    generator.exit(err);
    return;
  }

  const incompatibleDeps = {
    dependencies: {},
    devDependencies: {},
    peerDependencies: {},
  };

  let found = false;
  for (const d in templateDeps) {
    for (const s in incompatibleDeps) {
      const versionRange = pkgDeps[s][d];
      if (!versionRange) continue;
      const templateDep = templateDeps[d];
      // https://github.com/loopbackio/loopback-next/issues/2028
      // https://github.com/npm/node-semver/pull/238
      // semver.intersects does not like `*`, `x`, or `X`
      if (versionRange.match(/^\*|x|X/)) continue;
      if (generator.options.semver === false) {
        // For `lb4 update` command, check exact matches
        if (versionRange !== templateDep) {
          incompatibleDeps[s][d] = [versionRange, templateDep];
          found = true;
        }
        continue;
      }
      if (semver.intersects(versionRange, templateDep)) continue;
      incompatibleDeps[s][d] = [versionRange, templateDep];
      found = true;
    }
  }
  if (!found) {
    // No incompatible dependencies
    if (generator.command === 'update') {
      generator.log(
        chalk.green(
          `The project dependencies are compatible with @loopback/cli@${cliPkg.version}`,
        ),
      );
    }
    return;
  }

  const originalCliVersion = generator.config.get('version') || '<unknown>';
  generator.log(
    chalk.red(
      g.f(
        'The project was originally generated by @loopback/cli@%s.',
        originalCliVersion,
      ),
    ),
  );

  generator.log(
    chalk.red(
      g.f(
        'The following dependencies are incompatible with @loopback/cli@%s:',
        cliPkg.version,
      ),
    ),
  );
  for (const s in incompatibleDeps) {
    generator.log(s);
    for (const d in incompatibleDeps[s]) {
      generator.log(
        chalk.yellow('- %s: %s (cli %s)'),
        d,
        ...incompatibleDeps[s][d],
      );
    }
  }
  return incompatibleDeps;
}

/**
 * Update project dependencies with module versions from the cli template
 * @param pkg - Package json object for the project
 * @param generator - Yeoman generator instance
 */
function updateDependencies(generator) {
  const pkg =
    generator.packageJson.getAll() ||
    generator.fs.readJSON(generator.destinationPath('package.json'));
  const depUpdates = [];
  for (const d in templateDeps) {
    if (
      pkg.dependencies &&
      pkg.dependencies[d] &&
      pkg.dependencies[d] !== templateDeps[d]
    ) {
      depUpdates.push(
        `- Dependency ${d}: ${pkg.dependencies[d]} => ${templateDeps[d]}`,
      );
      pkg.dependencies[d] = templateDeps[d];
    }
    if (
      pkg.devDependencies &&
      pkg.devDependencies[d] &&
      pkg.devDependencies[d] !== templateDeps[d]
    ) {
      depUpdates.push(
        `- DevDependency ${d}: ${pkg.devDependencies[d]} => ${templateDeps[d]}`,
      );
      pkg.devDependencies[d] = templateDeps[d];
    }
    if (
      pkg.peerDependencies &&
      pkg.peerDependencies[d] &&
      pkg.peerDependencies[d] !== templateDeps[d]
    ) {
      depUpdates.push(
        `- PeerDependency ${d}: ${pkg.peerDependencies[d]} => ${templateDeps[d]}`,
      );
      pkg.peerDependencies[d] = templateDeps[d];
    }
  }
  if (depUpdates.length) {
    depUpdates.sort().forEach(d => generator.log(d));
  }
  generator.log(
    chalk.red('Upgrading dependencies may break the current project.'),
  );
  generator.fs.writeJSON(generator.destinationPath('package.json'), pkg);
  // Remove `node_modules` force a fresh install
  if (generator.command === 'update' && !generator.options['skip-install']) {
    fse.removeSync(generator.destinationPath('node_modules'));
  }
  generator.pkgManagerInstall();
}

/**
 * Check the LoopBack project dependencies and versions
 * @param generator - Yeoman generator instance
 */
async function checkLoopBackProject(generator) {
  if (generator.shouldExit()) return false;

  const incompatibleDeps = await checkDependencies(generator);
  if (incompatibleDeps == null) return false;
  if (
    Object.keys({
      ...incompatibleDeps.dependencies,
      ...incompatibleDeps.devDependencies,
      ...incompatibleDeps.peerDependencies,
    }) === 0
  )
    return false;

  const choices = [
    {
      name: g.f('Upgrade project dependencies'),
      value: 'upgrade',
    },
    {
      name: g.f('Skip upgrading project dependencies'),
      value: 'continue',
    },
  ];
  if (generator.command !== 'update') {
    choices.unshift({
      name: g.f('Abort now'),
      value: 'abort',
    });
  }
  const prompts = [
    {
      name: 'decision',
      message: g.f('How do you want to proceed?'),
      type: 'list',
      choices,
      default: 0,
    },
  ];
  const answers = await generator.prompt(prompts);
  if (answers && answers.decision === 'continue') {
    return false;
  }
  if (answers && answers.decision === 'upgrade') {
    updateDependencies(generator);
    return true;
  }
  generator.exit(new Error('Incompatible dependencies'));
}

/**
 * Check if the current cli is out of date
 * @param log - Log function
 */
async function checkCliVersion(log = console.log) {
  const latestCliVersion = await latestVersion('@loopback/cli');
  if (latestCliVersion !== cliPkg.version) {
    const current = chalk.grey(cliPkg.version);
    const latest = chalk.green(latestCliVersion);
    const cmd = chalk.cyan(`npm i -g ${cliPkg.name}`);
    const message = `
Update available ${current} ${chalk.reset(' → ')} ${latest}
Run ${cmd} to update.`;
    log(message);
  } else {
    log(chalk.green(`${cliPkg.name}@${cliPkg.version} is up to date.`));
  }
}

exports.printVersions = printVersions;
exports.checkCliVersion = checkCliVersion;
exports.checkDependencies = checkDependencies;
exports.updateDependencies = updateDependencies;
exports.checkLoopBackProject = checkLoopBackProject;
exports.cliPkg = cliPkg;
